#!/usr/bin/perl

# orgshadow.pl -- script to convert an emacs org-mode file to ShadowPlan xml
# Copyright (C) 2007, 2008 Jeff Stautz
#
# Author: Jeff Stautz <jeff dot stautz at gmail dot com>
# Keywords: ShadowPlan, org-mode, Palm, Emacs
# Homepage: jeffstautz dot com
# Version: 0.2
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

#
# orgshadow.pl converts an emacs org-mode text file to a Shadow Plan XML file
#
# It takes an org-file as an argument and writes the Shadow Plan XML to stdout.
# Output can be piped wherever you like.
#
# Typical usage looks something like this:
#    > orgshadow.pl org_file.org > ShadowP-MyShadowFile.xml
#
# Resulting XML files can be opened and edited by Shadow Desktop.
#
# DEPENDENCIES:
# To use this script, Time::Local must be installed and accessible.
#
#
# NOTE: If you're having problems (XML can't be parsed by Shadow Desktop, for
#    example), it's most likely because your outline "skips" a level. Be sure
#    the $odd_stars var is set appropriately below and that your outline 
#    doesn't have any sections like:
#
#    * item
#    ***** sub-item
#    *** sub-item 2
#
# TODO: I suppose I should fix this problem. 
#



#--------------------------------------------------------------
# OPTIONS
#
# Edit the options below before running orgshadow.pl
#--------------------------------------------------------------


#--------------------------------------------------------------
# Odd_stars
# If your outline only uses odd-numbered *s, set this var to 1
#--------------------------------------------------------------
$odd_stars = 1;


#--------------------------------------------------------------
# Keywords for determining a Shadow item's "start time" and "target time" 
# values (e.g. SCHEDULED or DEADLINE).
#
# These should be *arrays* of values: ('VALUE','VALUE2','VALUE3')
#
# If you don't wish to use one of these keywords -- for example, if you don't
# use start times in Shadow, set the variable to ('NULL').
#--------------------------------------------------------------
@start_time_keywords = ('NULL');
@target_time_keywords = ('SCHEDULED','DEADLINE');


#--------------------------------------------------------------
# These mutually exclusive flags determine whether timestamps in a headline
# item are treated as target or start dates. If timestamps in a headline item
# have no bearing, then set both to 0.
#--------------------------------------------------------------
$headline_timestamp_is_target = 1;
$headline_timestamp_is_start = 0;


#--------------------------------------------------------------
# Keywords for completion date
#
# Again, this is an array of possible "completion date" keywords
#--------------------------------------------------------------
@finish_time_keywords = ('CLOSED');


#--------------------------------------------------------------
# Keyword for completed items
#
# An array of possible "completed" keywords
#--------------------------------------------------------------
@completed_keywords = ('DONE');


#--------------------------------------------------------------
# Priority settings:
#--------------------------------------------------------------
$default_priority = 5;
$a_priority = 1;
$b_priority = 2;
$c_priority = 3;


#--------------------------------------------------------------
# End editable section
#
# *** Do not edit below this line. ***
#--------------------------------------------------------------

my $source = shift @ARGV;
open INPUT, $source or die "Error opening input file: $!";

# Load the whole org file into memory 
# TODO: consider changing this and building a byte-address index. If file is 
#       gigantic, this might help.
@lines = <INPUT>;

sub create_items;

print '<?xml version="1.0" encoding="iso-8859-1"?>' . "\n";
print '<ShadowPlanFile uniqueTime="0" uploadFile="1">' . "\n";

my $line_number = -1;
my $total_lines = (@lines - 1);
my $nest_level = 0;           # current level of "nesting" (depth in the tree)

while ($line_number < $total_lines )
{
    $line_number++;
    if ($lines[$line_number] =~ /^\*+/)
    {
	# If line starts with a *, we have an item here...
	# Find its level and create it
	$nest_level = length ($&);
	$line_number = create_items($line_number, $nest_level);
    }

}
print "</ShadowPlanFile>";


#--------------------------------------------------------------
# create_items function.
#
# Writes lines into the Shadow XML file for each org-mode item.
# Recursively calls itself to build children and siblings.
#
#  Arguments: $line_number    int    current line number
#             $nest_level     int    current level of depth in tree
#
#  Returns:   $line_number    int    the new current line number
#--------------------------------------------------------------

sub create_items
{
    my $line_number = shift;
    my $nest_level = shift;

    my $next_line_number = $line_number + 1;
    my $nest_count = 0;       # nesting level of *next* line
    my $indent = ' ';

	# Default values for item parameters
    my $note_buffer = '';
    my $title = '';
    my $priority = 0;
    my $checked = 'no';
    my $target_time = 0;
    my $start_time = 0;
    my $finish_time = 0;

    # How deep are we? Indent accordingly. 
	# NOTE: This is purely for aesthetics to keep the XML easy to read
    my $i = 0;
    while ($i < $nest_level)  
    { 
	    $indent = $indent . ' ';
	    $i++;
    }

    $title = trim($lines[$line_number]);
    $priority = getPriority($title);

	# See if line contains completion keyword
	# If it does, set $checked to true. If not, $checked is false
    $checked = isCompleted($lines[$line_number]);

	# Depending on user-defined options, set start or deadline values
    # Call getTime with 1 as the third param to denote we're on headline
    if ($headline_timestamp_is_target)
    {
	    $target_time = getTime($lines[$line_number], 'target', 1);
    }
    elsif ($headline_timestamp_is_start)
    {
	    $start_time = getTime($lines[$line_number], 'start', 1);
    }

    # While the next line isn't a headline item and isn't EOF, add the next
    # line to the notes buffer for this item.
    while ( ($next_line_number <= $total_lines) 
            && !($lines[$next_line_number] =~ /^\*+/) )
    {
	    $note_buffer = $note_buffer . $lines[$next_line_number];

		# Set item's start/target/finish time accordingly.
        # Call getTime with 0 as third param to show we're in the "notes" 
		# of an item.
		if (getTime($lines[$next_line_number], 'start', 0) != 0)
		{
		    $start_time = getTime($lines[$next_line_number], 'start', 0);
		}

		if (getTime($lines[$next_line_number], 'target', 0) != 0)
		{
		    $target_time = getTime($lines[$next_line_number], 'target', 0);
		}

		if (getTime($lines[$next_line_number], 'finish', 0) != 0)
		{
		    $finish_time = getTime($lines[$next_line_number], 'finish', 0);
		}

		$next_line_number++;
    }

    print $indent .  "<item ";
	

	# Set colour options: 
	#    Top-level items:  blue and bold
    #    2nd-level items:  brown and bold, memos expanded
	# TODO: make this user-customizable.
    if ($nest_level == 1)
    {
        print "dispBold=\"yes\" dispColor=\"95\" dispOverride=\"1\" ";
    }
    elsif ($nest_level == 2 || $nest_level == 3)
    {
        print "dispBold=\"yes\" dispColor=\"153\" expandedMemo=\"yes\" " . 
	  "dispOverride=\"1\" ";
    }
    else
    {
        print "dispOverride=\"3\" ";
    }

	# Set done state, priority, and print out title and notes.
    print "checked=\"" . $checked;
    print "\" priority=\"" . $priority . "\">\n";
    print $indent . "  " . "<title>" . "<![CDATA[" . $title . "]]>" .
	"</title>\n";
    if( length($note_buffer) > 0)
    {
        print $indent . "  <note>" . "<![CDATA[" . $note_buffer . "]]>" .
	  "</note>\n";
		$note_buffer = '';
    }

    print $indent . "  <hhTargetTime>" . $target_time . 
	"</hhTargetTime>\n";
    print $indent . "  <hhStartTime>" . $start_time . 
	"</hhStartTime>\n";
    print $indent . "  <hhFinishTime>" . $finish_time . 
	"</hhFinishTime>\n";

    # Special case 1: 
	# If the next line is EOF, close all items
    if ($next_line_number > $total_lines)
    {
		while ($nest_level > 0)
		{
			# TODO: figure out how to fix indentation issue here.
			print $indent . "</item>\n";

			# Go back one nesting level or two, depending on $odd_stars
			if ($odd_stars)
			{
				$nest_level = $nest_level - 2;
			}
			else
			{
				$nest_level--;
			}
		}
		return $line_number;
    }

    # Special Case 2:
	# If the next line is an outline item...
    if ($lines[$next_line_number] =~ /^\*+/)
    {
		# How deep is the next line?
		$nest_count = length ($&);
	
		if ($nest_count > $nest_level)
		{
			# Next line is a child item.
			$line_number = create_items($next_line_number, $nest_count);
		}
		elsif ($nest_count == $nest_level)
		{
			# Next line's a sibling.
			print $indent . "</item>\n";
			$line_number = create_items($next_line_number, $nest_level);
		}
		elsif ($nest_count < $nest_level)
		{
			# Next line's an uncle. (ancestor's sibling)
			while ($nest_level >= $nest_count)
			{
				# TODO: figure out how to fix indentation here.
				print $indent . "</item>\n";
				if ($odd_stars)
				{
					$nest_count = $nest_count + 2;
				}
				else
				{
					$nest_count++;
				}
			}
			
		}
		return $line_number;
    }
    else
    {	
		return $line_number;
    }
}


#--------------------------------------------------------------
# trim function.
#
# Removes whitespace from start and end of string. Also removes any *s from
# beginning of string.
#
#  Arguments: $string      string    String to be trimmed
#
#  Returns:   $string      string    Trimmed string
#--------------------------------------------------------------

sub trim($)
{
    my $string = shift;
    $string =~ s/^\*+//;
    $string =~ s/^\s+//;
    $string =~ s/\s+$//;
    return $string;
}


#--------------------------------------------------------------
# getPriority function.
#
# Grabs priority value from the title string.
#
#  Arguments: $title      string    Title of the item
#
#  Returns:   $priority   int       Prioirity value (as defined by user)
#--------------------------------------------------------------

sub getPriority($)
{
    $title = shift;
    $priority = $default_priority;
    if ($title =~ /\[#.\]/)
    {
	if($& eq '[#A]')
	{
	    $priority = $a_priority;
	}
	elsif($& eq '[#B]')
	{
	    $priority = $b_priority;
	}
	elsif($& eq '[#C]')
	{
	    $priority = $c_priority;
	}
    }
	return $priority;
}


#--------------------------------------------------------------
# isCompleted function.
#
# Searches line for any of the words in the user-defined array of "completed"
# keywords.
#
#  Arguments: $line            string    Title of the item
#
#  Returns:   $is_completed    string    'yes' if completed, 'no' if not.       
#--------------------------------------------------------------

sub isCompleted($)
{
    $line = shift;
    $is_completed = 'no';
    if (@completed_keywords ne NULL)
    {
	for $completed (@completed_keywords)
	{
	    if ($line =~ /\*\s$completed/)
	    {
		$is_completed = 'yes';
	    }		
	}
    }
    return $is_completed;
}



#--------------------------------------------------------------
# getTime function.
#
# Searches line for any of the words in the user-defined array of "completed"
# keywords.
#
#  Arguments: $line            string    Title of the item
#             $type            string    Type of timestamp we're looking for:
#                                        "start", "finish" or "target"
#             $on_headline     int       1 if on headline, 0 if not
#
#  Returns:   $date            int       Date in Palm Epoch format
#--------------------------------------------------------------

sub getTime($line, $type, $on_headline)
{
    $line = shift;
    $type = shift;
    $on_headline = shift;
    $date = 0;    

	# If on a headline, date's in < >
    if ($on_headline)
    {
		if ($line =~ /<[0-9]{4}-[0-9]{2}-[0-9]{2}.*>/)
		{
			$date = parseDate($&);
		}
    }

	# Otherwise, date in [ ] OR < >
    else
    {
		if ($type eq 'start')
		{
			$type = 'start_time_keywords';
		}
		elsif ($type eq 'finish')
		{
			$type = 'finish_time_keywords';
		}
		else 
		{
			$type = 'target_time_keywords';
		}

		if (@$type[0] ne 'NULL')
		{
			for $keyword (@$type)
			{
				if ($line =~ /$keyword:\s+\[[0-9]{4}-[0-9]{2}-[0-9]{2}.*\]/)
				{
					$date = parseDate($&);
				}
				elsif ($line =~ /$keyword:\s+\<[0-9]{4}-[0-9]{2}-[0-9]{2}.*\>/)
				{
					$date = parseDate($&);
				}
			}
		}
		return $date;
    }
}


#--------------------------------------------------------------
# parseDate function.
#
# Takes a date string in org-mode format and turns it into a Palm Epoch int
#
#  Arguments: $date_string     string    Date in org-mode format
#
#  Returns:   $palm_epoch      int       Date in Palm Epoch format
#--------------------------------------------------------------

sub parseDate($)
{
    $date_string = shift;

	# Date could have either [ or < delimiters.
    if ($date_string =~ /\[[0-9]{4}-[0-9]{2}-[0-9]{2}.*\]/)
    {
		$date_string = $&;
    }
    elsif ($date_string =~ /\<[0-9]{4}-[0-9]{2}-[0-9]{2}.*\>/)
    {
		$date_string = $&;
    }
    else
    {
		$date_string = '0000-00-00';
    }

    $year = substr($date_string, 1, 4);
    $month = substr($date_string, 6, 2);
    $day = substr($date_string, 9, 2);

    if ($date_string =~ /[0-9]{2}:[0-9]{2}/)
    {
		$hours = substr($&, 0, 2);
		$minutes = substr($&, 3, 2);
		$seconds = 0;
    }
    else
    {
		$hours = 0;
		$minutes = 0;
		$seconds = 0;
    }

	# Convert to Unix Epoch, then to Palm Epoch.
    use Time::Local;
    $unix_epoch = timelocal($seconds, $minutes, $hours, $day, $month-1, $year-1900);
    $palm_epoch = $unix_epoch + 2082844800;

    return $palm_epoch;
}
